(ns future-app.ios.calc
  (:require [reagent.core :refer [atom]]))

(defn input->char-type [input]
  (case input
    "=" :=
    "." :dot
    "%" :%
    "C" :clear
    "+/-" :+-
    (cond
      (number? input) :number
      (#{"+" "-" "*" "/"} input) :operator)))

(def calc-fsm
  ;; % +/- return old state
  (->> {:start {:= :start
                :number :input-operand1
                :operator :enter-operator
                :dot :input-operand1}
        :input-operand1 {:= :start
                         :number :input-operand1
                         :operator :enter-operator
                         :dot :input-operand1}
        :enter-operator {:= :enter-operator
                         :number :input-operand2
                         :operator :enter-operator
                         :dot :input-operand2}
        :input-operand2 {:= :evaled
                         :number :input-operand2
                         :operator :enter-operator
                         :dot :input-operand2}
        :evaled {:= :evaled
                 :number :input-operand1
                 :operator :enter-operator
                 :dot :input-operand1}}
       (map (fn [[k v]]
              {k (assoc v :+- k :% k)}))
       (into {})))

(defn next-state [current char-type]
  (get-in calc-fsm [current char-type]))

(def init {:operand1 0
           :operand2 nil
           :operator nil
           :decimal 0
           :state :start})

(def calc-db (atom init))

(defn do-calc [a b]
  ((case (:operator @calc-db)
     "+" +
     "-" -
     "*" *
     "/" /) a b))

(defn display-text []
  (let [num (if (#{:start :input-operand1 :enter-operator :evaled} (:state @calc-db))
              (:operand1 @calc-db)
              (:operand2 @calc-db))]
    (str (when (and (zero? num) (:neg? @calc-db))
           "-")
         num
         (when (and (zero? num) (> (:decimal @calc-db) 0))
           "."))))

(defn highlight? [label]
  (and (= (:state @calc-db) :enter-operator)
       (= (:operator @calc-db) label)))

(defn accumulate-operand [old input]
  (if (= 0 (:decimal @calc-db))
    ((if (neg? old) - +) (* old 10) input)
    ((if (neg? old) - +) old (* input (.pow js/Math 10 (- (:decimal @calc-db)))))))

(defn calc-handler [input]
  (if (= "C" input)
    (reset! calc-db init)
    (let [char-type (input->char-type input)]
      (case (:state @calc-db)
        :start (case char-type
                 (:% :+- :=) (prn input)  ; nothing to do here
                 :number (swap! calc-db assoc :operand1 input)
                 :operator (swap! calc-db assoc :operator input)
                 :dot (swap! calc-db assoc :decimal 1))
        :input-operand1 (case char-type
                          := (swap! calc-db assoc :decimal 0)
                          :number (swap! calc-db update :operand1 #(accumulate-operand % input))
                          :dot (swap! calc-db update :decimal #(if (> % 0) % 1))
                          :% (swap! calc-db update :operand1 #(/ % 100))
                          :+- (swap! calc-db update :operand1 #(- %))
                          :operator (do (swap! calc-db assoc :operator input)
                                        (swap! calc-db assoc :decimal 0)))
        :enter-operator (case char-type
                          := (swap! calc-db update :operand1 #(do-calc % %))
                          :number (do (swap! calc-db assoc :operand2 input)
                                      (swap! calc-db assoc :decimal 0))
                          :dot (do (swap! calc-db assoc :operand2 0)
                                   (swap! calc-db assoc :decimal 1))
                          :operator (swap! calc-db assoc :operator input)
                          :% (swap! calc-db update :operand1 #(/ % 100))
                          :+- (swap! calc-db update :operand1 #(- %)))
        :input-operand2 (case char-type
                          := (do (swap! calc-db update :operand1 #(do-calc % (:operand2 @calc-db)))
                                 (swap! calc-db assoc :decimal 0))
                          :number (swap! calc-db update :operand2 #(accumulate-operand % input))
                          :dot (swap! calc-db update :decimal #(if (> % 0) % 1))
                          :% (swap! calc-db update :operand2 #(/ % 100))
                          :+- (swap! calc-db update :operand2 #(- %))
                          :operator (do (swap! calc-db update :operand1 #(do-calc % (:operand2 @calc-db)))
                                        (swap! calc-db assoc :operator input)
                                        (swap! calc-db assoc :decimal 0)))
        :evaled (case char-type
                  := (swap! calc-db update :operand1 #(do-calc % (:operand2 @calc-db)))
                  :number (swap! calc-db assoc :operand1 input)
                  :dot (do (swap! calc-db assoc :operand1 0)
                           (swap! calc-db assoc :decimal 1))
                  :operator (swap! calc-db assoc :operator input)
                  :% (swap! calc-db update :operand1 #(/ % 100))
                  :+- (swap! calc-db update :operand1 #(- %))))
      (swap! calc-db update :state #(next-state % char-type)))))


